feat: show badge on top liked packages, link to leaderboard#2459
feat: show badge on top liked packages, link to leaderboard#2459
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
2 Skipped Deployments
|
Lunaria Status Overview🌕 This pull request will trigger status changes. Learn moreBy default, every PR changing files present in the Lunaria configuration's You can change this by adding one of the keywords present in the Tracked Files
Warnings reference
|
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
bbf8518 to
2163460
Compare
b4b05d2 to
3ac3b69
Compare
📝 WalkthroughSummary by CodeRabbit
WalkthroughA new likes leaderboard feature is introduced with a dedicated page route that fetches and displays ranked packages by total likes. The likes component is enhanced to track a package's top rank position. New server utilities fetch leaderboard data from an external API, enrich entries with npm packument metadata and Microlink homepage previews, and retrieve individual package rank information. Internationalisation strings and cache configurations are added to support the feature. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant PageRoute as Leaderboard Page
participant Handler as Leaderboard Handler
participant LeaderboardAPI as External Leaderboard API
participant NpmRegistry as npm Registry
participant MicrolicAPI as Microlink API
participant GitHubAPI as GitHub API
Client->>PageRoute: Load /leaderboard/likes
PageRoute->>Handler: GET /api/leaderboard/likes
Handler->>LeaderboardAPI: Fetch likes leaderboard (limit=10)
LeaderboardAPI-->>Handler: LikesLeaderboardEntry[]
loop For each leaderboard entry
Handler->>NpmRegistry: Fetch packument (description, homepage)
NpmRegistry-->>Handler: Package metadata
Handler->>MicrolicAPI: Fetch homepage metadata (preview/logo)
MicrolicAPI-->>Handler: HomepageMetadata
Handler->>GitHubAPI: Fetch repo stars & weekly downloads
GitHubAPI-->>Handler: Stars & download count
end
Handler-->>PageRoute: Enriched LikesLeaderboardEntry[]
PageRoute-->>Client: Render podium + ranked list
sequenceDiagram
participant Component as Likes Component
participant Handler as Likes Handler
participant RankAPI as Rank Lookup
participant LikesUtil as Likes Util
Component->>Handler: GET /api/social/likes/[...pkg]
par Concurrent Fetch
Handler->>LikesUtil: getLikes(event, packageName)
LikesUtil-->>Handler: {totalLikes, userHasLiked}
and Concurrent Fetch
Handler->>RankAPI: getTopLikedRank(event, subjectRef)
RankAPI-->>Handler: number | null
end
Handler-->>Component: {totalLikes, userHasLiked, topLikedRank}
Component->>Component: Compute UI labels (like/unlike, tooltip, badge text)
Component-->>Component: Render likes counter + top-rank badge (if rank exists)
Suggested reviewers
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Show a small rank badge next to the likes counter/button when a package is in the top 10 most-liked packages, and link that badge to a new in-app likes leaderboard page. For now at least, this is the only way to reach the leaderboard page. Both are powered by server-side fetching of the likes leaderboard API (https://tangled.org/baileytownsend.dev/npmx-likes-leaderboard), maintained by Bailey. Fetches degrade gracefully: no badge is shown on the package page, and the leaderboard page shows a message indicating that the data is unavailable. Successful fetches are cached for 1 hour, and are only revalidated in the background, following a stale-while-revalidate pattern (this is existing behaviour from `server/plugins/fetch-cache`). The leaderboard page is itself cached with ISR, with a revalidation time of 15 minutes.
This defers this very optional fetch to later and avoids caching degraded/failed responses.
3ac3b69 to
a4c4d6a
Compare
| } | ||
|
|
||
| const repositoryRef = parseRepoUrl(rawRepositoryUrl) | ||
| if (!repositoryRef || repositoryRef.provider !== 'github') { |
There was a problem hiding this comment.
We do support a dozen git providers, but... pragmatically, we know the top, like... 1000 most liked packages are on GitHub, so since this is just for the top 10 it didn't seem worth overengineering right now.
| const leaderboard = await getLikesLeaderboard(event) | ||
| return leaderboard?.find(entry => entry.subjectRef === subjectRef)?.rank ?? null |
There was a problem hiding this comment.
This may look stupid from a perf perspective but getLikesLeaderboard is cached for an hour (with ISR semantics) so I think it's quite fine.
262c38d to
d5859e9
Compare
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (10)
shared/utils/constants.ts (1)
19-21: Consider making the leaderboard API URL configurable via runtime config.
LIKES_LEADERBOARD_API_URLhardcodes a third-party Railway deployment URL. If the API host ever moves (different region, custom domain, staging environment, or a self-hosted alternative), this will require a code change and redeploy. Exposing it viaruntimeConfig.public(with this constant as the default) would give operators a runtime escape hatch.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@shared/utils/constants.ts` around lines 19 - 21, The constant LIKES_LEADERBOARD_API_URL hardcodes a deployment URL; change it to be configurable at runtime by reading a public runtime config value (e.g. runtimeConfig.public.likesLeaderboardApiUrl) with the existing string as the default. Replace the exported constant with a getter (or export a function like getLikesLeaderboardApiUrl) that returns runtimeConfig.public.likesLeaderboardApiUrl || 'https://npmx-likes-leaderboard-api-production.up.railway.app/api/leaderboard/likes', and update all call sites that import LIKES_LEADERBOARD_API_URL to call the getter/function instead so operators can override the URL without redeploying. Ensure you reference runtime config APIs used by your app (e.g., useRuntimeConfig or access process.env if appropriate) when implementing the getter.i18n/locales/en.json (1)
801-801: Hardcoded10in description couples copy to API limit.
"The 10 most liked packages on npmx right now."hardcodes the leaderboard size. If the?limit=10constant is ever changed (or made configurable), this string will go out of sync silently. Consider parameterising with a{count}placeholder driven by the same source as the API request.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@i18n/locales/en.json` at line 801, The description string "The 10 most liked packages on npmx right now." hardcodes the leaderboard size; replace it with a parameterised placeholder like "{count}" and wire the same count value used by the API request (the limit query param or limit constant) into the i18n formatter so the displayed number comes from the same variable that sets ?limit=10; update the locale key (the "description" entry) and ensure the view/controller that builds the request passes that count into the i18n translate/format call so the text and the API request remain in sync.server/api/social/likes/[...pkg].get.ts (1)
27-36: Parallel fetch reads cleanly; relies ongetTopLikedRanknever throwing.The current
getTopLikedRankimplementation is safe (it bottoms out ingetLikesLeaderboard's try/catch returningnull), soPromise.allwon't reject due to leaderboard failures. If a future refactor ever letsgetTopLikedRankthrow, likes responses would 502 — considerPromise.allSettledor a local try/catch around the rank fetch to harden the contract that the rank is "best-effort, never blocks likes".♻️ Optional defensive variant
- const [likes, topLikedRank] = await Promise.all([ - likesUtil.getLikes(packageName, oAuthSession?.did.toString()), - getTopLikedRank(event, PACKAGE_SUBJECT_REF(packageName)), - ]) - - return { - ...likes, - topLikedRank, - } + const [likesResult, rankResult] = await Promise.allSettled([ + likesUtil.getLikes(packageName, oAuthSession?.did.toString()), + getTopLikedRank(event, PACKAGE_SUBJECT_REF(packageName)), + ]) + if (likesResult.status === 'rejected') throw likesResult.reason + return { + ...likesResult.value, + topLikedRank: rankResult.status === 'fulfilled' ? rankResult.value : null, + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@server/api/social/likes/`[...pkg].get.ts around lines 27 - 36, The current parallel fetch uses Promise.all with likesUtil.getLikes and getTopLikedRank which assumes getTopLikedRank never throws; make the rank fetch best-effort so failures don’t cause the whole handler to 502 by wrapping the rank call in a protective construct (either use Promise.allSettled for the two promises and extract a successful topLikedRank or null on rejection, or perform a separate try/catch around await getTopLikedRank(event, PACKAGE_SUBJECT_REF(packageName)) and set topLikedRank = null on error); keep likesUtil.getLikes (PackageLikesUtils) awaited normally and return the combined result with topLikedRank defaulting to null on failure.server/api/leaderboard/likes.get.ts (1)
5-10: Consider falling back to unenriched entries on enrichment failure.If
enrichLikesLeaderboardEntriesrejects (e.g. one of the per-entry homepage/stars fetches throws and isn't internally caught), the entire endpoint errors and the page loses data we already successfully fetched from the upstream leaderboard. A small wrapper around the enrichment call would let you serve raw entries as a graceful fallback, matching the PR's stated "degrade gracefully" intent.♻️ Suggested fallback
- const leaderboardEntries = await getLikesLeaderboard(event) - if (!leaderboardEntries) return [] - - return await enrichLikesLeaderboardEntries(event, leaderboardEntries) + const leaderboardEntries = await getLikesLeaderboard(event) + if (!leaderboardEntries) return [] + + try { + return await enrichLikesLeaderboardEntries(event, leaderboardEntries) + } catch (err) { + console.error('[leaderboard/likes] Enrichment failed, returning raw entries:', err) + return leaderboardEntries + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@server/api/leaderboard/likes.get.ts` around lines 5 - 10, The endpoint currently calls enrichLikesLeaderboardEntries(event, leaderboardEntries) and will error the whole request if enrichment rejects; wrap that call in a try/catch so you return the raw leaderboardEntries on enrichment failure. Specifically, in the eventHandler after getLikesLeaderboard returns entries, call enrichLikesLeaderboardEntries inside a try block and return its result if successful, but on catch log the error (or processLogger/error) and return leaderboardEntries as the graceful fallback; keep the initial getLikesLeaderboard and the function signatures unchanged.server/utils/likes-leaderboard.ts (2)
90-156: Reduce duplication and length ingetLeaderboardEntryMetadata.The function is 67 lines (above the ~50-line guideline) primarily because the same return object shape is built three times (lines 124–129, 134–139, 142–147). Collapsing it into a single
returnwith the GitHub ref derived inline keeps all field assignments in one place and shrinks the function meaningfully.♻️ Suggested shape
- if (!rawRepositoryUrl) { - return { - packageDescription, - weeklyDownloads, - homepageUrl, - githubRepositoryRef: null, - } - } - - const repositoryRef = parseRepoUrl(rawRepositoryUrl) - if (!repositoryRef || repositoryRef.provider !== 'github') { - return { - packageDescription, - weeklyDownloads, - homepageUrl, - githubRepositoryRef: null, - } - } - - return { - packageDescription, - weeklyDownloads, - homepageUrl, - githubRepositoryRef: repositoryRef, - } + const repositoryRef = rawRepositoryUrl ? parseRepoUrl(rawRepositoryUrl) : null + return { + packageDescription, + weeklyDownloads, + homepageUrl, + githubRepositoryRef: + repositoryRef?.provider === 'github' ? repositoryRef : null, + }As per coding guidelines: "Keep functions focused and manageable (generally under 50 lines)".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@server/utils/likes-leaderboard.ts` around lines 90 - 156, The getLeaderboardEntryMetadata function duplicates the same return shape three times; refactor it to build all fields once and return a single object: compute encodedPackageName, packument, downloadsResult and parsedDownloads as now, derive rawRepositoryUrl and let repositoryRef = rawRepositoryUrl ? parseRepoUrl(rawRepositoryUrl) : null, then set githubRepositoryRef = repositoryRef && repositoryRef.provider === 'github' ? repositoryRef : null and return { packageDescription, weeklyDownloads, homepageUrl, githubRepositoryRef } in one place (keeping the existing try/catch and use of cachedFetch, NpmDownloadCountSchema and encodePackageName).
41-50: Regex matches sub-paths; consider tightening.
^https://npmx\.dev/package/(.+)$will accepthttps://npmx.dev/package/foo/extra/segmentsand capture the entire trailing portion as the package name. This would only matter if upstream ever returns malformed/extra-segmentedsubjectRefvalues, but a stricter pattern (e.g. capturing only@scope/nameor unscoped names) plus an explicit reject for unexpected trailing characters would fail loudly rather than silently produce a junkpackageName. Optional given the controlled upstream contract.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@server/utils/likes-leaderboard.ts` around lines 41 - 50, The regex in extractPackageNameFromSubjectRef is too permissive and will capture extra path segments; tighten it to only accept valid npm package names (either unscoped [^/]+ or scoped `@scope/name`) and reject any trailing characters so malformed subjectRef like /package/foo/extra are not accepted; update the pattern used in extractPackageNameFromSubjectRef to match either `@scope/name` or name (e.g. ^https://npmx\.dev/package/(@[^/]+\/[^/]+|[^/]+)$) and keep the existing decodeURIComponent/try-catch logic for returning the package name or null on non-match.app/pages/leaderboard/likes.vue (2)
119-119: Minor: prefer numericv-forover hardcoded arrays.Vue's
v-for="n in 3"iteratesn = 1, 2, 3and avoids allocating a literal array on every render. For the 4–10 range you can usev-for="n in 7"and key onn + 3, orArray.from({ length: 7 }, (_, i) => i + 4).♻️ Proposed tweak
- <li v-for="rank in [1, 2, 3]" :key="rank" class="space-y-4"> + <li v-for="rank in 3" :key="rank" class="space-y-4">- <li v-for="rank in [4, 5, 6, 7, 8, 9, 10]" :key="rank"> + <li v-for="i in 7" :key="i + 3"> + <!-- use (i + 3) wherever `rank` was referenced -->Also applies to: 166-166, 214-214
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/pages/leaderboard/likes.vue` at line 119, Replace the hardcoded array v-for uses with numeric v-for to avoid allocating arrays on each render: change the top-three loop to v-for="rank in 3" and keep :key="rank"; for the 4–10 block replace the array with v-for="n in 7" and compute the displayed rank and key as n + 3 (or use a computed value) so keys remain unique; make the same replacement for the other two occurrences mentioned (the blocks at the other v-for locations) ensuring you update any template expressions that referenced the original array values to use the new loop variable arithmetic.
281-461: Consider extracting the podium card to remove ~180 lines of duplication.The mobile (
lg:hidden) and desktop (hidden lg:grid) podium<ol>blocks render identical card content forhighlightedEntries; only the outer list classes andgetPodiumItemClassdiffer. The same duplication exists in the skeleton section (lines 118–160 vs. 162–211). Extracting the per-entry card into a small<PodiumCard>(or av-slot-driven sub-component) would cut the template roughly in half and make future changes (e.g. metric tweaks, i18n strings) only need to be applied in one place.Note: The current setup does also have all top-3 metric/title strings duplicated three times, so any tweak (e.g. changing
text-xs→text-sm) is currently a four-place edit (mobile data, desktop data, mobile skeleton, desktop skeleton).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/pages/leaderboard/likes.vue` around lines 281 - 461, The template duplicates the entire per-entry card for mobile and desktop (and their skeletons); extract the repeated card into a new PodiumCard component (or functional sub-component) that accepts props like entry (or packageName, homepagePreviewUrl, homepagePreviewWidth/Height, repositoryStars, weeklyDownloads, totalLikes, rank) and exposes a slot for skeleton rendering; keep the outer <li> wrapper (where getPodiumItemClass(entry.rank) is applied) in the parent lists and replace the repeated BaseCard/inner markup with <PodiumCard :entry="entry" :rank="entry.rank" :to="packageRoute(entry.packageName)" /> (and use the same for skeletons), ensuring PodiumCard uses compactNumberFormatter, formatCompactStat and $t internally so metric/i18n changes are centralized.server/utils/npm-homepage.ts (1)
95-97: Consider logging unexpected Microlink errors for observability.The bare
catch {}swallows everything — including timeouts and unexpected runtime errors — without any signal. The sibling utility inserver/utils/likes-leaderboard.ts(lines 207–213) usesconsole.error('[likes-leaderboard] Failed to fetch likes leaderboard:', ...)for symmetry; consider matching that here so silent Microlink outages are diagnosable in production logs.📝 Optional logging
- } catch { + } catch (err) { + console.error( + '[npm-homepage] Failed to fetch homepage metadata:', + err instanceof Error ? err.message : 'Unknown error', + ) return emptyHomepageMetadata(homepageUrl) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@server/utils/npm-homepage.ts` around lines 95 - 97, The empty catch in server/utils/npm-homepage.ts swallows Microlink errors; change it to catch the error (e) and log it before returning emptyHomepageMetadata(homepageUrl), e.g. use console.error with a clear prefix like '[npm-homepage] Failed to fetch Microlink:' and include the error object so outages/timeouts are observable; keep the return of emptyHomepageMetadata(homepageUrl) unchanged.test/unit/server/utils/likes-leaderboard.spec.ts (1)
280-295: Add coverage forgetTopLikedRanknull/no-match paths.The single test only covers a happy-path match. Worth adding cases for:
- Subject ref not present in the leaderboard → returns
null.- Upstream
cachedFetchrejects (sogetLikesLeaderboardreturnsnull) →getTopLikedRankalso returnsnull.These are cheap to add and would protect the
?? nullfallback on line 252 ofserver/utils/likes-leaderboard.tsfrom a future refactor.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@test/unit/server/utils/likes-leaderboard.spec.ts` around lines 280 - 295, Add two tests in test/unit/server/utils/likes-leaderboard.spec.ts for getTopLikedRank: (1) mock cachedFetch to resolve to a leaderboard that does NOT include the queried subjectRef (use cachedResult with leaderBoard array missing the entry) and assert getTopLikedRank(createEvent(cachedFetch), '<subjectRef>') returns null; (2) mock cachedFetch to reject (vi.fn().mockRejectedValue(new Error(...))) so getLikesLeaderboard returns null, then assert getTopLikedRank(createEvent(cachedFetch), '<subjectRef>') returns null; reference getTopLikedRank, getLikesLeaderboard, createEvent and cachedFetch when adding these specs.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/components/Package/Likes.vue`:
- Around line 113-118: The merge is incorrectly treating null as missing for
topLikedRank; change the assignment in likesData.value so topLikedRank is set to
result.data.topLikedRank when the server explicitly returned null (i.e.,
preserve null), and only fall back to previousLikesState.topLikedRank when the
property is actually absent — use a presence check on result.data (e.g.,
'topLikedRank' in result.data or hasOwnProperty) rather than nullish coalescing;
update the block that spreads previousLikesState and result.data (referencing
likesData.value, previousLikesState, result.data, and topLikedRank) accordingly.
In `@test/unit/server/utils/likes-leaderboard.spec.ts`:
- Around line 137-165: The test uses the Packument type (referenced in
packuments and the fetchNpmPackageMock implementation) but it isn't imported;
add a type-only import for Packument from '#shared/types' at the top of the file
(so Packument is available for the packuments object and the mocked return
typing) to resolve the missing type reference.
In `@test/unit/server/utils/npm-homepage.spec.ts`:
- Around line 22-30: The test fixture loader function loadMicrolinkFixture uses
__dirname which breaks in ESM; replace its usage by deriving a directory from
import.meta.url (the import.meta.url -> fileURLToPath -> path.dirname pattern
used elsewhere) and use that directory when building fixturePath; update
loadMicrolinkFixture to compute const __dirnameEquivalent =
path.dirname(fileURLToPath(import.meta.url)) and then use that variable instead
of __dirname (apply the same change in the analogous test file
test/unit/server/utils/docs/format.spec.ts).
---
Nitpick comments:
In `@app/pages/leaderboard/likes.vue`:
- Line 119: Replace the hardcoded array v-for uses with numeric v-for to avoid
allocating arrays on each render: change the top-three loop to v-for="rank in 3"
and keep :key="rank"; for the 4–10 block replace the array with v-for="n in 7"
and compute the displayed rank and key as n + 3 (or use a computed value) so
keys remain unique; make the same replacement for the other two occurrences
mentioned (the blocks at the other v-for locations) ensuring you update any
template expressions that referenced the original array values to use the new
loop variable arithmetic.
- Around line 281-461: The template duplicates the entire per-entry card for
mobile and desktop (and their skeletons); extract the repeated card into a new
PodiumCard component (or functional sub-component) that accepts props like entry
(or packageName, homepagePreviewUrl, homepagePreviewWidth/Height,
repositoryStars, weeklyDownloads, totalLikes, rank) and exposes a slot for
skeleton rendering; keep the outer <li> wrapper (where
getPodiumItemClass(entry.rank) is applied) in the parent lists and replace the
repeated BaseCard/inner markup with <PodiumCard :entry="entry"
:rank="entry.rank" :to="packageRoute(entry.packageName)" /> (and use the same
for skeletons), ensuring PodiumCard uses compactNumberFormatter,
formatCompactStat and $t internally so metric/i18n changes are centralized.
In `@i18n/locales/en.json`:
- Line 801: The description string "The 10 most liked packages on npmx right
now." hardcodes the leaderboard size; replace it with a parameterised
placeholder like "{count}" and wire the same count value used by the API request
(the limit query param or limit constant) into the i18n formatter so the
displayed number comes from the same variable that sets ?limit=10; update the
locale key (the "description" entry) and ensure the view/controller that builds
the request passes that count into the i18n translate/format call so the text
and the API request remain in sync.
In `@server/api/leaderboard/likes.get.ts`:
- Around line 5-10: The endpoint currently calls
enrichLikesLeaderboardEntries(event, leaderboardEntries) and will error the
whole request if enrichment rejects; wrap that call in a try/catch so you return
the raw leaderboardEntries on enrichment failure. Specifically, in the
eventHandler after getLikesLeaderboard returns entries, call
enrichLikesLeaderboardEntries inside a try block and return its result if
successful, but on catch log the error (or processLogger/error) and return
leaderboardEntries as the graceful fallback; keep the initial
getLikesLeaderboard and the function signatures unchanged.
In `@server/api/social/likes/`[...pkg].get.ts:
- Around line 27-36: The current parallel fetch uses Promise.all with
likesUtil.getLikes and getTopLikedRank which assumes getTopLikedRank never
throws; make the rank fetch best-effort so failures don’t cause the whole
handler to 502 by wrapping the rank call in a protective construct (either use
Promise.allSettled for the two promises and extract a successful topLikedRank or
null on rejection, or perform a separate try/catch around await
getTopLikedRank(event, PACKAGE_SUBJECT_REF(packageName)) and set topLikedRank =
null on error); keep likesUtil.getLikes (PackageLikesUtils) awaited normally and
return the combined result with topLikedRank defaulting to null on failure.
In `@server/utils/likes-leaderboard.ts`:
- Around line 90-156: The getLeaderboardEntryMetadata function duplicates the
same return shape three times; refactor it to build all fields once and return a
single object: compute encodedPackageName, packument, downloadsResult and
parsedDownloads as now, derive rawRepositoryUrl and let repositoryRef =
rawRepositoryUrl ? parseRepoUrl(rawRepositoryUrl) : null, then set
githubRepositoryRef = repositoryRef && repositoryRef.provider === 'github' ?
repositoryRef : null and return { packageDescription, weeklyDownloads,
homepageUrl, githubRepositoryRef } in one place (keeping the existing try/catch
and use of cachedFetch, NpmDownloadCountSchema and encodePackageName).
- Around line 41-50: The regex in extractPackageNameFromSubjectRef is too
permissive and will capture extra path segments; tighten it to only accept valid
npm package names (either unscoped [^/]+ or scoped `@scope/name`) and reject any
trailing characters so malformed subjectRef like /package/foo/extra are not
accepted; update the pattern used in extractPackageNameFromSubjectRef to match
either `@scope/name` or name (e.g.
^https://npmx\.dev/package/(@[^/]+\/[^/]+|[^/]+)$) and keep the existing
decodeURIComponent/try-catch logic for returning the package name or null on
non-match.
In `@server/utils/npm-homepage.ts`:
- Around line 95-97: The empty catch in server/utils/npm-homepage.ts swallows
Microlink errors; change it to catch the error (e) and log it before returning
emptyHomepageMetadata(homepageUrl), e.g. use console.error with a clear prefix
like '[npm-homepage] Failed to fetch Microlink:' and include the error object so
outages/timeouts are observable; keep the return of
emptyHomepageMetadata(homepageUrl) unchanged.
In `@shared/utils/constants.ts`:
- Around line 19-21: The constant LIKES_LEADERBOARD_API_URL hardcodes a
deployment URL; change it to be configurable at runtime by reading a public
runtime config value (e.g. runtimeConfig.public.likesLeaderboardApiUrl) with the
existing string as the default. Replace the exported constant with a getter (or
export a function like getLikesLeaderboardApiUrl) that returns
runtimeConfig.public.likesLeaderboardApiUrl ||
'https://npmx-likes-leaderboard-api-production.up.railway.app/api/leaderboard/likes',
and update all call sites that import LIKES_LEADERBOARD_API_URL to call the
getter/function instead so operators can override the URL without redeploying.
Ensure you reference runtime config APIs used by your app (e.g.,
useRuntimeConfig or access process.env if appropriate) when implementing the
getter.
In `@test/unit/server/utils/likes-leaderboard.spec.ts`:
- Around line 280-295: Add two tests in
test/unit/server/utils/likes-leaderboard.spec.ts for getTopLikedRank: (1) mock
cachedFetch to resolve to a leaderboard that does NOT include the queried
subjectRef (use cachedResult with leaderBoard array missing the entry) and
assert getTopLikedRank(createEvent(cachedFetch), '<subjectRef>') returns null;
(2) mock cachedFetch to reject (vi.fn().mockRejectedValue(new Error(...))) so
getLikesLeaderboard returns null, then assert
getTopLikedRank(createEvent(cachedFetch), '<subjectRef>') returns null;
reference getTopLikedRank, getLikesLeaderboard, createEvent and cachedFetch when
adding these specs.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: dea7b5aa-7f1f-4ee9-a5e5-5305dfbd15dc
📒 Files selected for processing (25)
app/components/Package/Likes.vueapp/pages/leaderboard/likes.vuei18n/locales/en.jsoni18n/locales/fr-FR.jsoni18n/schema.jsonnuxt.config.tsserver/api/leaderboard/likes.get.tsserver/api/social/likes/[...pkg].get.tsserver/utils/atproto/utils/likes.tsserver/utils/likes-leaderboard.tsserver/utils/npm-homepage.tsshared/types/social.tsshared/utils/constants.tsshared/utils/fetch-cache-config.tstest/fixtures/likes-leaderboard.tstest/fixtures/microlink/kit.svelte.dev.jsontest/fixtures/microlink/nuxt.com.jsontest/fixtures/microlink/react.dev.jsontest/fixtures/microlink/vuejs.org.jsontest/fixtures/mock-routes.cjstest/nuxt/a11y.spec.tstest/nuxt/components/Package/Likes.spec.tstest/nuxt/pages/LikesLeaderboardPage.spec.tstest/unit/server/utils/likes-leaderboard.spec.tstest/unit/server/utils/npm-homepage.spec.ts
🔗 Linked issue
N/A 😶
🧭 Context
Social likes are fun. Having users engage with the community, discover packages, and share socially about like ranks is fun.
📚 Description
Show a small rank badge next to the likes counter/button when a package is in the top 10 most-liked packages, and link that badge to a new in-app likes leaderboard page. For now at least, this is the only way to reach the leaderboard page.
npmx.top.liked.demo.v2.mp4
Both are powered by server-side fetching of the likes leaderboard API (https://tangled.org/baileytownsend.dev/npmx-likes-leaderboard), maintained by Bailey (@fatfingers23), who has agreed to treat this as a production service.
API fetches degrade gracefully: on failure, no badge is shown on the package page, and the leaderboard page shows a message indicating that the data is unavailable.
Successful fetches are cached for 1 hour, and are only revalidated in the background, following a
stale-while-revalidate-style pattern (this is existing behaviour fromserver/plugins/fetch-cache).The leaderboard page is itself cached with ISR, with a revalidation time of 15 minutes.
Here's a fallback screen in case of missing data or failure to load the data: